﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Nonocast.Http {
	public interface SmallHTTPService {
		int Port { get; }
		SmallHTTPService Open();
		void Close();
		void Receive(TcpClient client, string data);
		void Notify(string data);
	}

	public class SmallHTTPServiceBase : SmallHTTPService {
		public int Port { get; private set; }

		public virtual SmallHTTPService Open() {
			listener = new TcpListener(IPAddress.Any, Port);
			listener.Start();
			listener.BeginAcceptTcpClient(new AsyncCallback(AcceptCallback), listener);
			return this;
		}

		public virtual void Close() {
			listener.Stop();
		}

		public virtual void Receive(TcpClient client, string data) { }

		public virtual void Notify(string data) {
			List<ServiceChannel> channels = null;
			lock (doublewayChannels) { channels = new List<ServiceChannel>(doublewayChannels); }
			foreach (var each in channels) {
				try {
					each.Notify(data);
				} catch {
					each.Close();
				}
			}
		}

		private void AcceptCallback(IAsyncResult ar) {
			var listener = ar.AsyncState as TcpListener;

			try {
				var client = listener.EndAcceptTcpClient(ar);
				if (client != null) {
					new Thread(new ParameterizedThreadStart(NewTcpClient)).Start(client);
				}
			} catch (ObjectDisposedException) {
				// ignore
			} finally {
				try { listener.BeginAcceptTcpClient(new AsyncCallback(AcceptCallback), listener); } catch { }
			}
		}

		private void NewTcpClient(object obj) {
			ConsoleHelper.WriteLine(ConsoleColor.Red, "Thread Enter\t(Total: {0})", ++threadCount);

			var client = obj as TcpClient;
			var stream = client.GetStream();
			ServiceChannel channel = null;
			try {
				var header = new ServiceRequestHeader(stream);
				ConsoleHelper.Write(header);

				var rule = createChannelRules.FirstOrDefault(p => p.IsMatch(header));
				if (rule != null) {
					channel = rule.Create(header);
					channel.Stream = stream;
					channel.Client = client;

					contexts.Value = new ChannelContext { Client = client, Stream = stream, Header = header };

					if (channel.IsDoubleWay) {
						lock (doublewayChannels) { doublewayChannels.Add(channel); }
					}
					ConsoleHelper.WriteLine(ConsoleColor.DarkCyan, "+ {0}", channel.GetType().Name);
					channel.Run();
				} else {
					Send404(stream);
				}
			} catch (Exception ex) {
				ConsoleHelper.WriteLine(ConsoleColor.Yellow, "Thread Exception - {0}", ex.Message);
			} finally {
				if (channel != null && channel.IsDoubleWay) {
					lock (doublewayChannels) {
						if (doublewayChannels.Contains(channel)) {
							doublewayChannels.Remove(channel);
						}
					}
				}
				try { stream.Close(); } catch { }
				try { client.Close(); } catch { }
				ConsoleHelper.WriteLine(ConsoleColor.Green, "Thread Exit\t(Total: {0})", --threadCount);
			}
		}

		private void Send404(NetworkStream stream) {
			string message = "<h1>Error</h1>";
			string responseTemplate = "HTTP/1.1 404 NotFound\r\nConnection:close\r\nContent-Type: text/html\r\nContent-Length: {0}\r\n\r\n{1}";
			var response = string.Format(responseTemplate, message.Length, message);
			var buffer = Encoding.UTF8.GetBytes(message);
			stream.Write(buffer, 0, buffer.Length);
		}

		public SmallHTTPServiceBase() {
			Port = 7005;
			createChannelRules = new List<CreateChannelRule> {
				new CreateStaticResourceChannelRule(this),
				new CreateActionChannelRule(this),
				new CreateWebSocketDraft76ChannelRule(this),
				new CreateWebSocketRFC6455ChannelRule(this)
			};
			ChannelContext.Contexts = contexts;
		}

		public SmallHTTPServiceBase(int port)
			: this() {
			Port = port;
		}

		private List<ServiceChannel> doublewayChannels = new List<ServiceChannel>();
		private List<CreateChannelRule> createChannelRules;
		private TcpListener listener;
		private static int threadCount;
		private static ThreadLocal<ChannelContext> contexts = new ThreadLocal<ChannelContext>();
	}

	internal interface ServiceChannel {
		bool IsDoubleWay { get; }
		NetworkStream Stream { get; set; }
		TcpClient Client { get; set; }

		void Run();
		void Close();
		void Notify(string data);
	}

	internal interface CreateChannelRule {
		bool IsMatch(ServiceRequestHeader header);
		ServiceChannel Create(ServiceRequestHeader header);
	}
}
